from collections import namedtuple
import readline
import traceback
from typing import Optional

from absl import app

import coin.proto.account_control_pb2 as control_proto
from xunkemgmt.support.accounting.logic.query_request import (AccountRequest)
from coin.support.account_control.dispatcher import dispatch_control_request
from coin.support.account_control.util import account_request_to_control_target

Command = namedtuple('Command', ['cmd', 'cmd_callback', 'help_info'])


def _check_account_request(account_request):
  try:
    assert account_request
    assert account_request.market_type and account_request.exchange and \
           account_request.owner
    return True
  except Exception:
    print('Account not set.')
    return False


def _args_check(arg_num):
  def decorator(func):
    def wrapper(*args):
      if len(args) != arg_num + 2:
        print('This command needs %s argument.' % arg_num)
        return
      return func(*args)

    return wrapper

  return decorator


def _get_account_operation_type(operation):
  operation_type_map = {
      'query_balance': control_proto.QUERY_ACCOUNT_BALANCE,
      'query_position': control_proto.QUERY_ACCOUNT_POSITION,
      'query_order': control_proto.QUERY_ORDERS,  #'query_fill': control_proto.QUERY_FILLS,
      'cancel_all': control_proto.CANCEL_ALL,  #'clear_all': control_proto.CLEAR_ALL,
      'net_zero': control_proto.NET_ZERO
  }
  assert operation in operation_type_map, \
    'Fail to find operation type: %s.' % operation
  return operation_type_map[operation]


class SimpleCompleter(object):
  def __init__(self, options):
    self.options = sorted(options)
    return

  def complete(self, text, state):
    response = None
    if state == 0:
      if text:
        self.matches = [s for s in self.options if s and s.startswith(text)]
      else:
        self.matches = self.options[:]
    try:
      response = self.matches[state]
    except IndexError:
      response = None
    return response


class CmdLineTradeController(object):
  def __init__(self):
    self._cmd_handler_map = {}
    self._exit = False
    self._ready_product = None
    self._ready_account_request = None
    self._balance = None
    self._position = None
    self._working_order = None
    self.initialize()

  def run(self):
    self.prompt_command()

  def prompt_command(self):
    while True:
      try:
        if self._exit:
          break
        cmd = input('cmd> ').strip()
        if not cmd:
          continue
        self.handle_command(cmd)
        readline.add_history(cmd)
      except Exception as e:
        print('%s failed. ' % cmd, type(e), e)
        print(traceback.format_exc())

  def handle_command(self, cmd):
    params = cmd.split()
    if params[0] in self._cmd_handler_map:
      self._cmd_handler_map[params[0]].cmd_callback(*params)
    else:
      print('No such command.')

  def register_command_handler(self, cmd, handler, help_info):
    assert cmd not in self._cmd_handler_map
    self._cmd_handler_map[cmd] = Command(cmd, handler, help_info)

  def initialize(self):
    self.register_command_handler('set_account', self.set_account_request, '')
    self.register_command_handler('set_product', self.set_product, '')
    self.register_command_handler('query_balance', self.process_account_operation_request, '')
    self.register_command_handler('query_position', self.process_account_operation_request, '')
    self.register_command_handler('query_order', self.process_account_operation_request, '')
    self.register_command_handler('cancel_all', self.process_account_operation_request, '')
    self.register_command_handler('net_zero', self.process_account_operation_request, '')
    self.register_command_handler('approach_position', self.process_approach_position_request, '')
    self.register_command_handler('show', self.show_setting, '')
    self.register_command_handler('balance', self.show_balance, '')
    self.register_command_handler('position', self.show_position, '')
    self.register_command_handler('order', self.show_working_order, '')
    self.register_command_handler('help', self.show_help, '')
    self.register_command_handler('exit', self.exit, '')
    """
    self.register_command_handler(
        'query_fill', self.process_account_operation_request, '')
    self.register_command_handler(
        'clear_all', self.process_account_operation_request, '')
    """
    readline.set_completer(SimpleCompleter(list(self._cmd_handler_map.keys())).complete)
    readline.parse_and_bind('tab: complete')

  def process_approach_position_request(self, *args):
    account_request = self._ready_account_request
    assert _check_account_request(account_request), 'Account not set.'
    assert self._ready_product is not None, 'Product not set'
    start_position = input('cmd> <start_position> ').strip()
    target_position = input('cmd> <target_position> ').strip()
    pass_bp = input('cmd> <pass_bp> ').strip()
    lot_size = input('cmd> <lot_size> ').strip()
    stickiness = input('cmd> <stickiness> ').strip()
    order_update_period = input('cmd> <order_update_period> ').strip()
    control_request = self._gen_approach_position_control_proto(start_position,
                                                                target_position,
                                                                pass_bp,
                                                                lot_size,
                                                                stickiness,
                                                                order_update_period)
    control_response = dispatch_control_request(control_request)

  def process_account_operation_request(self, *args):
    operation = args[0]
    account_request = self._ready_account_request
    assert _check_account_request(account_request), 'Account not set.'
    operation_type = _get_account_operation_type(operation)
    if operation_type in (control_proto.QUERY_ORDERS,
                          control_proto.CANCEL_ALL,
                          control_proto.NET_ZERO):
      assert self._ready_product is not None, 'Product not set'
    control_request = self._gen_account_operation_control_proto(operation_type)
    control_response = dispatch_control_request(control_request)
    self._update_by_account_operation_response(control_response)

  @_args_check(0)
  def set_account_request(self, *args):
    account_request = self._ready_account_request
    exchange = input('cmd> <exchange> ').strip() or account_request.exchange
    market_type = input('cmd> <market_type> ').strip() or \
                  account_request.market_type
    api_version = input('cmd> <api_version> ').strip() or \
                  account_request.api_version
    owner = input('cmd> <owner> ').strip() or account_request.owner
    self._ready_account_request = AccountRequest(market_type=market_type,
                                                 exchange=exchange,
                                                 api_version=api_version,
                                                 owner=owner,
                                                 accounting_currency=None)
    self._reset_account_info()

  @_args_check(1)
  def set_product(self, *args):
    self._ready_product = args[1]

  @_args_check(0)
  def show_setting(self, *args):
    print('Exchange: %s\tMarket_type: %s\tOwner: %s\tProduct: %s' %
          (self._ready_account_request.exchange,
           self._ready_account_request.market_type,
           self._ready_account_request.owner,
           self._ready_product))

  @_args_check(0)
  def show_balance(self, *args):
    self.print_balance()

  @_args_check(0)
  def show_position(self, *args):
    self.print_position()

  @_args_check(0)
  def show_working_order(self, *args):
    self.print_working_order()

  @_args_check(0)
  def show_help(self, *args):
    for command in sorted(self._cmd_handler_map.values()):
      print('%-20s\t%s' % (command.cmd, command.help_info))

  @_args_check(0)
  def exit(self, *args):
    self._exit = True

  def print_balance(self):
    account_balance = self._balance
    if account_balance is None:
      print('Balance unavailable.')
    else:
      print('Exchange: %s\tMarket_type: %s' %
            (account_balance.exchange, account_balance.market_type))
      for balance in account_balance.each_balance:
        print('Currency: %s\tTotal: %s' % (balance.currency, balance.total))

  def print_position(self):
    account_position = self._position
    if account_position is None:
      print('Position unavailable.')
    else:
      print('Exchange: %s\tMarket_type: %s' %
            (account_position.exchange, account_position.market_type))
      for position in account_position.each_position:
        print('Symbol: %s\tLong: %s\tShort: %s\tNet: %s' % (
            position.symbol, position.long_position, position.short_position,
            position.net_position))

  def print_working_order(self):
    product_order_bundle = self._working_order
    if product_order_bundle is None:
      print('Working order unavailable.')
    else:
      print('Exchange: %s\tProduct: %s' %
            (product_order_bundle.exchange, product_order_bundle.symbol))
      for order in product_order_bundle.each_order:
        print('Symbol: %s\tPrice: %s\tQty: %s\tSide: %s\tOrder_id: %s\t' %
              (order.symbol, order.price, order.qty, order.side, order.order_id))

  def _gen_approach_position_control_proto(self,
                                           start_position,
                                           target_position,
                                           pass_bp,
                                           lot_size,
                                           stickiness,
                                           order_update_period):
    target = account_request_to_control_target(self._ready_account_request, self._ready_product)
    approach_position = control_proto.ApproachPositionRequest(
        start_position=float(start_position),
        target_position=float(target_position),
        pass_bp=float(pass_bp),
        lot_size=float(lot_size),
        stickiness=float(stickiness),
        order_update_period=float(order_update_period))
    control_request = control_proto.ControlRequest(target=target,
                                                   type=control_proto.CONTROL_APPROACH_POSITION,
                                                   approach_position=approach_position)
    return control_request

  def _gen_account_operation_control_proto(self, operation_type):
    target = account_request_to_control_target(self._ready_account_request, self._ready_product)
    account_operation = control_proto.AccountOperationRequest(operation_type=operation_type)
    control_request = control_proto.ControlRequest(target=target,
                                                   type=control_proto.CONTROL_ACCOUNT_OPERATION,
                                                   account_operation=account_operation)
    return control_request

  def _update_by_account_operation_response(self, control_response: control_proto.ControlResponse):
    assert control_response.type == control_proto.CONTROL_ACCOUNT_OPERATION
    operation_response = control_response.account_operation
    target = control_response.target
    assert target.market_type == self._ready_account_request.market_type and \
           target.exchange == self._ready_account_request.exchange and \
           target.owner == self._ready_account_request.owner
    if target.HasField('product'):
      assert target.product == self._ready_product
    operation_type = operation_response.operation_type
    if operation_type == control_proto.QUERY_ACCOUNT_BALANCE:
      self._set_balance(operation_response)
      self.print_balance()
    if operation_type == control_proto.QUERY_ACCOUNT_POSITION:
      self._set_position(operation_response)
      self.print_position()
    if operation_type == control_proto.QUERY_ORDERS:
      self._set_working_order(operation_response)
      self.print_working_order()

  def _set_balance(self, operation_response: Optional[control_proto.AccountOperationResponse]):
    if operation_response is not None:
      operation_type = operation_response.operation_type
      success = operation_response.success
      assert operation_type == control_proto.QUERY_ACCOUNT_BALANCE
      self._balance = operation_response.account_balance if success else None
    else:
      self._balance = None

  def _set_position(self, operation_response: Optional[control_proto.AccountOperationResponse]):
    if operation_response is not None:
      operation_type = operation_response.operation_type
      success = operation_response.success
      assert operation_type == control_proto.QUERY_ACCOUNT_POSITION
      self._position = operation_response.account_position if success else None
    else:
      self._position = None

  def _set_working_order(self,
                         operation_response: Optional[control_proto.AccountOperationResponse]):
    if operation_response is not None:
      operation_type = operation_response.operation_type
      success = operation_response.success
      assert operation_type == control_proto.QUERY_ORDERS
      self._working_order = operation_response.product_order if success \
                            else None
    else:
      self._balance = None

  def _reset_account_info(self):
    self._set_balance(None)
    self._set_position(None)


def main(_):
  cmd_line_controller = CmdLineTradeController()
  cmd_line_controller.run()


if __name__ == '__main__':
  app.run(main)
